--- id: TASK-043 title: 'Grammar checking via Harper (harper-ls, green undercurl)' status: "\U0001F3C1 Done" assignee: [] created_date: '2026-07-14 02:45' updated_date: '2026-07-14 15:25' labels: - feature dependencies: [] priority: medium ordinal: 42000 --- ## Description Add grammar checking with green undercurl underlines, alongside the red-undercurl spellcheck (TASK-020). APPROACH (decided): - Engine: Harper (writewithharper.com). Spawn harper-ls as an LSP subprocess over stdio — keeps glint zero-cgo (harper-core is Rust; no FFI bind). - Dependency model: OPTIONAL / auto-detect. Grammar turns on only if harper-ls is found on PATH; silently off otherwise. Power users opt in via `brew install harper`. No Homebrew formula coupling. - Rendering: REUSE existing undercurl span infra (Span.Wavy + Span.UnderColor, raw SGR from TASK-020). Add theme.Grammar (green) beside theme.Spell (red) in internal/theme/theme.go + all 3 palettes in themes.go. Grammar diagnostics render as Wavy spans with UnderColor=theme.Grammar. PLUMBING: - Minimal LSP client (one file, internal/grammar/): initialize -> didOpen -> didChange (debounced on edit) -> receive textDocument/publishDiagnostics. - Map LSP diagnostic ranges (line/char) to editor rune ranges -> green undercurl spans, layered in a pass like spellPass (internal/editor/spellcheck.go:135). Mind: selection overrides Wavy; cursor cell suppresses undercurl (span.go). - Session toggle like SetSpell/ToggleSpell; likely a keybind + config flag. Grammar suggestions could reuse the suggest popup (internal/app/spell.go) later. OPEN QUESTIONS: - Debounce interval / async model in the Bubbletea loop (harper-ls replies asynchronously; need a tea.Cmd + msg for diagnostics). - Harper license check before bundling any config. - Overlap policy when a word is both misspelled (red) and in a grammar span (green). ## Acceptance Criteria - [x] #1 Grammar underlines render green, distinct from red spellcheck - [x] #2 harper-ls run as optional LSP subprocess; feature off cleanly when binary absent - [x] #3 Zero-cgo build and Homebrew formula unchanged - [x] #4 theme.Grammar added to all themes - [x] #5 Debounced diagnostics update on edit without blocking the UI - [x] #6 Session toggle to enable/disable grammar checking ## Implementation Notes RESOLVED (probed harper-ls 2.6.0 live): - Sync: textDocumentSync.change=1 (Full) -> didChange sends whole buffer, version++. - Handshake: initialize -> initialized. MUST answer server->client requests or diagnostics never come: workspace/configuration -> reply [{}] (array len = items), client/registerCapability -> reply null. Ignore other server requests with null. - Diagnostics: textDocument/publishDiagnostics, diagnostics[].range{start,end}{line,character(UTF-16)}, message, code, source=Harper, severity 4(Hint). Render ALL as green undercurl. - character offsets are UTF-16 code units -> map utf16->rune per line. - Use file's real dir as rootUri (missing dir logs a non-fatal backend error). - Toggle UX (decided): add 'Toggle grammar' row to the Alt+; spell popup (no new global key). Config: grammar=auto|on|off, default auto=on iff harper-ls on PATH. - Overlap: red spelling wins; grammar green only paints spans not already Wavy.